package es.ubu.ceudaaj.exercise.ud02.t04.l03;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Class representing a list of contacts that can be read and written into and
 * from an xml file.
 * 
 * This version uses DOM to process the xml.
 * 
 */
public class AgendaDOM implements Agenda {

	private static final String XML_INPUT_PATH = "src/main/resources/xml/Contactos.xml";
	private static final String XML_OUTPUT_PATH = "src/main/resources/xml/Contactos_new.xml";

	// Attribute to keep a reference to the current XML in memory.
	private Document doc;

	private static AgendaDOM instance = null;

	/**
	 * Private constructor This class should only be used as singleton
	 */
	private AgendaDOM() {
	}

	/**
	 * Returns a DOM based agenda
	 * 
	 * @return AgendaDOM
	 */
	public static AgendaDOM getInstance() {
		if (instance == null)
			instance = new AgendaDOM();
		return instance;
	}

	/**
	 * Shows a list of current contacts on console. (Only name and surnames).
	 */
	public ArrayList<GUIDataModel> listContacts() {
		NodeList contacts;
		ArrayList<GUIDataModel> gdm = new ArrayList<GUIDataModel>();
		if (doc != null
				&& (contacts = doc.getElementsByTagName("Contactos")) != null
				&& contacts.getLength() > 0) {
			contacts = doc.getElementsByTagName("Contactos");
			for (int i = 0; i < contacts.getLength(); i++) {
				gdm.add(toGUIDataModel((Element) contacts.item(i)));
			}
		} else {
			System.out.println("(lista vacia)");
		}
		return gdm;
	}

	/**
	 * Prints detailed information of selected user on console.
	 * 
	 * @param index
	 *            Position of selected contact.
	 * @throws IOException
	 */
	public GUIDataModel printContactDetail(int index) throws IOException {
		NodeList contacts;
		if (doc != null
				&& ((contacts = doc.getElementsByTagName("Contactos")) != null)
				&& contacts.getLength() >= index && index > 0) {
			Element cont = (Element) contacts.item(index - 1);
			return toGUIDataModel(cont);
		} else {
			System.out.println("No existe en la lista de contactos.");
			return null;
		}
	}

	/**
	 * Creates a new contact and adds it to the XML representation in memory.
	 * 
	 * @throws IOException
	 */
	public void createNewContact(GUIDataModel gdm) throws IOException {
		if (doc == null) {
			DocumentBuilderFactory factory = DocumentBuilderFactory
					.newInstance();
			try {
				doc = factory.newDocumentBuilder().newDocument();
				Element dataroot = doc.createElement("dataroot");
				SimpleDateFormat sdf = new SimpleDateFormat(
						"yyyy-MM-dd'T'HH:mm:ss");
				String date = sdf.format(new Date());
				dataroot.setAttribute("generated", date);
				doc.appendChild(dataroot);
			} catch (ParserConfigurationException e) {
				System.err.println("Error al crear el documento XML.");
			}
		}

		Node dataroot = doc.getElementsByTagName("dataroot").item(0);

		Element contact = doc.createElement("Contactos");

		Element contactId = doc.createElement("IdContacto");
		contactId.appendChild(doc.createTextNode(String.valueOf(doc
				.getElementsByTagName("Contactos").getLength() + 1)));
		contact.appendChild(contactId);

		Element contactSurnames = doc.createElement("Apellidos");
		contactSurnames.appendChild(doc.createTextNode(gdm.apellidos));
		contact.appendChild(contactSurnames);

		Element contactName = doc.createElement("Nombre");
		contactName.appendChild(doc.createTextNode(gdm.nombre));
		contact.appendChild(contactName);

		Element contactPhone = doc.createElement("TelefonoTrabajo");
		contactPhone.appendChild(doc.createTextNode(gdm.telefono));
		contact.appendChild(contactPhone);

		Element contactMail = doc.createElement("NomCorreoElectronico");
		contactMail.appendChild(doc.createTextNode(gdm.email));
		contact.appendChild(contactMail);

		Element jobEspecif = doc.createElement("Cargo");
		jobEspecif.appendChild(doc.createTextNode(gdm.cargo));
		contact.appendChild(jobEspecif);

		Element codPostal = doc.createElement("CodPostal");
		codPostal.appendChild(doc.createTextNode(gdm.codPostal));
		contact.appendChild(codPostal);

		dataroot.appendChild(contact);
	}

	/**
	 * Method for editing the information for the selected contact in the XML
	 * representation in memory.
	 * 
	 * @param index
	 *            Position of selected contact.
	 * @throws IOException
	 */
	public boolean editContact(GUIDataModel gdm) throws IOException {
		NodeList contacts;
		int index = Integer.parseInt(gdm.id);
		if (doc != null
				&& ((contacts = doc.getElementsByTagName("Contactos")) != null)
				&& contacts.getLength() >= index && index > 0) {

			Element cont = (Element) contacts.item(index - 1);
			cont.getElementsByTagName("Nombre").item(0)
					.setTextContent(gdm.nombre);
			cont.getElementsByTagName("Apellidos").item(0)
					.setTextContent(gdm.apellidos);
			cont.getElementsByTagName("TelefonoTrabajo").item(0)
					.setTextContent(gdm.telefono);
			cont.getElementsByTagName("NomCorreoElectronico").item(0)
					.setTextContent(gdm.email);
			cont.getElementsByTagName("Cargo").item(0)
					.setTextContent(gdm.cargo);
			cont.getElementsByTagName("CodPostal").item(0)
					.setTextContent(gdm.codPostal);
			return true;
		} else {
			System.out.println("No existe en la lista de contactos.");
			return false;
		}
	}

	/**
	 * Removes the selected contact from the XML representation in memory.
	 * 
	 * @param index
	 *            Position of selected contact.
	 */
	public void deleteContact(int index) {
		NodeList contacts;
		if (doc != null
				&& ((contacts = doc.getElementsByTagName("Contactos")) != null)
				&& contacts.getLength() >= index && index > 0) {
			Element cont = (Element) contacts.item(index - 1);
			doc.getDocumentElement().removeChild(cont);
			// cont.getParentNode().removeChild(cont);
			System.out.println("Contacto borrado.");
		} else {
			System.out.println("No existe en la lista de contactos.");
		}
	}

	/**
	 * Writes the current XML in a file.
	 */
	public boolean saveXML() {
		if (doc != null) {
			TransformerFactory transformerFactory = TransformerFactory.newInstance();
			Transformer transformer;
			try {
				transformer = transformerFactory.newTransformer();
				DOMSource source = new DOMSource(doc);
				StreamResult result = new StreamResult(
						new File(XML_OUTPUT_PATH));
				transformer.transform(source, result);
				System.out.println("Se ha guardado correctamente el fichero "
						+ XML_OUTPUT_PATH);
				return true;
			} catch (TransformerConfigurationException e) {
				System.err.println("Error guardando el fichero XML: " + e);
			} catch (TransformerException e) {
				System.err.println("Error guardando el fichero XML." + e);
			}
		} else {
			System.out.println("Aun no existe ningun contacto");
		}
		return false;
	}

	/**
	 * Reads the input XML file.
	 */
	public void loadXML() {
		// Create a DOM builder and parse the fragment
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		try {
			doc = factory.newDocumentBuilder().parse(new File(XML_INPUT_PATH));
			System.out.println("Se ha leido el fichero " + XML_INPUT_PATH);
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Search contacts with the given name in the XML in memory.
	 * 
	 * Uses XPath for the search.
	 * 
	 * @param name
	 *            Name of the contact to search.
	 */
	public ArrayList<GUIDataModel> searchByName(String name) {
		String predicate = "Nombre='" + name + "'";
		return xpathSearch(predicate);
	}

	/**
	 * Search contacts with the given job in the XML in memory.
	 * 
	 * Uses XPath for the search.
	 * 
	 * @param name
	 *            Job of the contact to search.
	 */
	public ArrayList<GUIDataModel> searchByJob(String job) {
		String predicate = "Cargo='" + job + "'";
		return xpathSearch(predicate);
	}

	/**
	 * Search contacts with the given postal code in the XML in memory.
	 * 
	 * Uses XPath for the search.
	 * 
	 * @param name
	 *            Postal code of the contact to search.
	 */
	public ArrayList<GUIDataModel> searchByPostalCode(String postalCode) {
		String predicate = "CodPostal='" + postalCode + "'";
		return xpathSearch(predicate);
	}

	/**
	 * Prepares a query to evaluate with XPath and executes the query.
	 * 
	 * @param predicate
	 *            The predicate specifying the specific search to do.
	 */
	public ArrayList<GUIDataModel> xpathSearch(String predicate) {
		if (doc != null) {
			return evaluateXPathQuery("//Contactos[" + predicate + "]");
		} else {
			System.out.println("Aun no hay contactos cargados");
			return new ArrayList<GUIDataModel>();
		}
	}

	/**
	 * Performs the given XPath query against the XML in memory.
	 * 
	 * @param query
	 *            XPath query.
	 * @return
	 */
	private ArrayList<GUIDataModel> evaluateXPathQuery(String query) {
		ArrayList<GUIDataModel> results = new ArrayList<GUIDataModel>();
		XPathFactory factory = null;
		XPath xpath = null;
		if (doc != null) {
			factory = XPathFactory.newInstance();
			xpath = factory.newXPath();
			try {
				XPathExpression expr = xpath.compile(query);
				Object result = expr.evaluate(doc, XPathConstants.NODESET);
				NodeList nodes = (NodeList) result;
				for (int i = 0; i < nodes.getLength(); i++) {
					results.add(toGUIDataModel((Element) nodes.item(i)));
				}
			} catch (XPathExpressionException ex) {
				System.err.println("Error al buscar el elemento: " + ex);
			}
		}
		return results;
	}

	/**
	 * Converts data to GUIDataModel
	 */
	public GUIDataModel toGUIDataModel(Element cont) {
		GUIDataModel gdm = new GUIDataModel();
		gdm.setId(cont.getElementsByTagName("IdContacto").item(0)
				.getTextContent());
		gdm.setNombre(cont.getElementsByTagName("Nombre").item(0)
				.getTextContent());
		gdm.setApellidos(cont.getElementsByTagName("Apellidos").item(0)
				.getTextContent());
		gdm.setCargo(cont.getElementsByTagName("Cargo").item(0)
				.getTextContent());
		gdm.setTelefono(cont.getElementsByTagName("TelefonoTrabajo").item(0)
				.getTextContent());
		gdm.setEmail(cont.getElementsByTagName("NomCorreoElectronico").item(0)
				.getTextContent());
		gdm.setCodPostal(cont.getElementsByTagName("CodPostal").item(0)
				.getTextContent());
		return gdm;
	}
	
	/**
	 * Returns a new instance with no XML loaded
	 * FOR TESTING PURPOSES ONLY
	 * @return
	 */
	public static AgendaDOM getEmptyInstance(){
		instance = new AgendaDOM();
		return instance;
	}
}